#include "config.h"

#ifdef WINDOWS

#pragma comment (lib,"dokan.lib")

#include <errno.h>
#include <stdio.h>
#include <fcntl.h>

#include <windows.h>
#include <dokan.h>
#include <boost/lexical_cast.hpp>

#include "Filesystem.h"


using namespace std;

PFilesystem pFilesystem;

FILETIME filetime;


/**
 *
 **/
void Filesystem::setFileHandler(PFSInfo info, PFileHandler fh)
{
	info->fi->Context = (ULONG64)fh;
}

/**
 *
 **/
PFileHandler Filesystem::getFileHandler(PFSInfo info)
{
	return (PFileHandler)info->fi->Context;
}

/**
 *
 **/
wstring str2wstr(String &s)
{
	WCHAR wchOut[MAX_PATH + 1]; //I know s will always be a path
	MultiByteToWideChar(CP_ACP, 0, s.c_str(), s.size() + 1, wchOut, sizeof(wchOut)/sizeof(wchOut[0]));
	return wchOut;
}

void setDefaultFindData(WIN32_FIND_DATAW *findData, String elementname)
{
	wstring wideElementName = str2wstr( elementname );
	wcscpy_s(findData->cFileName,( wideElementName.size()+1 ), wideElementName.c_str());

	findData->ftCreationTime = filetime;
	findData->ftLastAccessTime = filetime;
	findData->ftLastWriteTime = filetime;
}

/**
 *
 **/
void Filesystem::listDir(String dirname, PFSInfo info)
{
    WIN32_FIND_DATAW findData;

    setDefaultFindData(&findData, dirname);

	if(dirname.compare(this->getInternCacheDir()) == 0) {
		findData.dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY;
	} else {
		findData.dwFileAttributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_DIRECTORY;
	}

	info->fi->IsDirectory = true;
	info->filler(&findData, info->fi);
}

/**
 *
 **/
void Filesystem::listFile(String filename, PFSInfo info, size_t filesize)
{
	WIN32_FIND_DATAW findData;
    setDefaultFindData(&findData, filename);

	findData.dwFileAttributes = FILE_ATTRIBUTE_NORMAL;
	findData.nFileSizeHigh = 0;

	PMemoryFile file = this->getMemoryFile(info->path+PATH_SEPERATOR+filename, info->isCache);
	if(file != NULL) {
		findData.nFileSizeLow = file->size; // real filesize
	} else {
		findData.nFileSizeLow = filesize; // default filesize
	}

    info->fi->IsDirectory = false;
	info->filler(&findData, info->fi);
}

/**
 *
 **/
void Filesystem::setDirAttribute(PFSInfo info, bool readOnly) {
    info->fi->IsDirectory = true;

    info->attributes->ftCreationTime = filetime;
    info->attributes->ftLastAccessTime = filetime;
    info->attributes->ftLastWriteTime = filetime;

    if(readOnly)
    {
        info->attributes->dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_READONLY;
    }
    else
    {
        info->attributes->dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY;
    }
}

/**
 *
 **/
void Filesystem::setFileAttribute(PFSInfo info, size_t filesize) {
    info->fi->IsDirectory = false;

    info->attributes->ftCreationTime = filetime;
    info->attributes->ftLastAccessTime = filetime;
    info->attributes->ftLastWriteTime = filetime;

    info->attributes->dwFileAttributes = FILE_ATTRIBUTE_NORMAL;
    info->attributes->nFileSizeHigh = 0;

    if(info->isCache)
    {
        PMemoryFile file = pFilesystem->getMemoryFile(info->path, info->isCache);
        if(file != NULL) {
            info->attributes->nFileSizeLow = file->size;
        } else {
            info->attributes->nFileSizeLow = filesize;
        }
    }
    else info->attributes->nFileSizeLow = filesize; // Filesize
}

/**
 *
 **/
int DOKAN_CALLBACK geodbfs_readdir( LPCWSTR path, PFillFindData filler, PDOKAN_FILE_INFO dokanFileInfo) {
    FSInfo info = Filesystem::createFSInfo("readdir", path, NULL, NULL, 0, 0, 0, filler, NULL, dokanFileInfo, pFilesystem);
	return pFilesystem->readDir( &info );
}

/**
 *
 **/
int DOKAN_CALLBACK geodbfs_release( LPCWSTR path, PDOKAN_FILE_INFO dokanFileInfo) {
    FSInfo info = Filesystem::createFSInfo("release", path, NULL ,NULL, 0, 0, 0, NULL, NULL, dokanFileInfo, pFilesystem);
	PFileHandler fh = pFilesystem->getFileHandler( &info);
	if(fh != NULL)
	{
		if(fh->deleteOnClose == true)
		{
			pFilesystem->unlink( &info );
		}
	}
	return 0;
}

/**
 *
 **/

#define MirrorCheckFlag(val, flag) if (val&flag) { Filesystem::debug("\t" #flag ""); }
/**
 *
 **/
 #define DbgPrint(name) wcout << name

int DOKAN_CALLBACK geodbfs_open(LPCWSTR path, DWORD AccessMode, DWORD shareMode, DWORD creationDisposition, DWORD flagsAndAttributes, PDOKAN_FILE_INFO dokanFileInfo) {
	FSInfo info = Filesystem::createFSInfo("open", path, NULL, NULL, 0, 0, 0, NULL, NULL, dokanFileInfo, pFilesystem);
	int ret1 = 0;

    info.readMode   = (AccessMode & FILE_READ_ATTRIBUTES  ) == FILE_READ_ATTRIBUTES  || ( AccessMode & FILE_READ_DATA  ) == FILE_READ_DATA  || ( AccessMode & FILE_READ_EA  ) == FILE_READ_EA;
    info.writeMode  = (AccessMode & FILE_WRITE_ATTRIBUTES ) == FILE_WRITE_ATTRIBUTES || ( AccessMode & FILE_WRITE_DATA ) == FILE_WRITE_DATA || ( AccessMode & FILE_WRITE_EA ) == FILE_WRITE_EA ||creationDisposition == CREATE_NEW || creationDisposition == CREATE_ALWAYS;
    info.appendMode = (AccessMode & FILE_APPEND_DATA) == FILE_APPEND_DATA;
    info.rwMode     = info.writeMode == true && info.readMode == true;
    info.createMode = creationDisposition == CREATE_NEW || creationDisposition == CREATE_ALWAYS;

	if(info.isCache && info.level == 2 && creationDisposition == OPEN_EXISTING)
	{
		if(pFilesystem->getMemoryFile(info.path, true) == NULL)
		{
			ret1 = -ENOENT; // File not exists
		}
	}

	int ret2 = pFilesystem->open( &info );

	if(AccessMode & DELETE)
	{
		PFileHandler fh = pFilesystem->getFileHandler(&info);
		if(fh == NULL) fh = new FileHandler;
		fh->deleteOnClose = true;
	}
	else
	{
        for (int i=0; i<pFilesystem->unlinkList.size(); i++) {
            if(info.path.compare(pFilesystem->unlinkList[i]) == 0)
            {
                return -ENOENT;
            }
        }
	}

    if (creationDisposition == TRUNCATE_EXISTING || creationDisposition == CREATE_ALWAYS) {
        pFilesystem->truncate(&info);
    }

	if( ret2 != 0) {
		return ret2;
	} else {
		return ret1;
	}
}


/**
 *
 **/
int DOKAN_CALLBACK geodbfs_write(LPCWSTR path, LPCVOID writeBuffer, DWORD BufferLength, LPDWORD WriteLength, LONGLONG offset, PDOKAN_FILE_INFO dokanFileInfo)
{
    FSInfo info = Filesystem::createFSInfo("write", path, NULL, writeBuffer, BufferLength, WriteLength, offset, NULL, NULL, dokanFileInfo, pFilesystem);
	DWORD size = pFilesystem->write( &info );
	if(size < 0) return size;
    *WriteLength = size;
	return 0;
}

/**
 *
 **/
int DOKAN_CALLBACK geodbfs_read(LPCWSTR path, LPVOID readBuffer, DWORD BufferLength, LPDWORD ReadLength, LONGLONG offset, PDOKAN_FILE_INFO dokanFileInfo) {
	FSInfo info = Filesystem::createFSInfo("read", path, readBuffer, NULL, BufferLength, ReadLength, offset, NULL, NULL, dokanFileInfo, pFilesystem);
	DWORD size = pFilesystem->read( &info );
	
	if(size < 0)
	{
		return size; // errors
	}
	*ReadLength = size;
	return 0;
}

/**
 *
 **/
int DOKAN_CALLBACK geodbfs_close(LPCWSTR path, PDOKAN_FILE_INFO dokanFileInfo) {
	FSInfo info = Filesystem::createFSInfo("close", path, NULL, NULL, 0, 0, 0, NULL, NULL, dokanFileInfo, pFilesystem);
	return pFilesystem->close( &info );
}

/**
 *
 **/
int DOKAN_CALLBACK geodbfs_getattr(LPCWSTR path, LPBY_HANDLE_FILE_INFORMATION attr, PDOKAN_FILE_INFO dokanFileInfo) {
	FSInfo info = Filesystem::createFSInfo("getattr", path, NULL, NULL, 0, 0, 0, NULL, attr, dokanFileInfo, pFilesystem);
	return pFilesystem->getattr( &info );
}

/**
 *
 **/
int DOKAN_CALLBACK geodbfs_opendir(LPCWSTR path, PDOKAN_FILE_INFO dokanFileInfo){
	FSInfo info = Filesystem::createFSInfo("opendir", path, NULL, NULL, 0, 0, 0, NULL, NULL, dokanFileInfo, pFilesystem);


    return pFilesystem->openDir(&info);
}

/**
 *
 **/
int DOKAN_CALLBACK geodbfs_getVolumeInformation(LPWSTR VolumeNameBuffer,DWORD VolumeNameSize, LPDWORD VolumeSerialNumber, LPDWORD MaximumComponentLength, LPDWORD FileSystemFlags, LPWSTR FileSystemNameBuffer, DWORD FileSystemNameSize, PDOKAN_FILE_INFO dokanFileInfo)
{
		wstring name = str2wstr("GeoDBFS_"+pFilesystem->getDBConnection()->getDatabase());
        wcscpy_s(VolumeNameBuffer, VolumeNameSize / sizeof(WCHAR), name.c_str());
        *VolumeSerialNumber = 0x19831116;
        *MaximumComponentLength = 256;
        *FileSystemFlags = FILE_CASE_SENSITIVE_SEARCH | FILE_CASE_PRESERVED_NAMES | FILE_SUPPORTS_REMOTE_STORAGE | FILE_UNICODE_ON_DISK;

        wcscpy_s(FileSystemNameBuffer, FileSystemNameSize / sizeof(WCHAR), name.c_str());

        return 0;
}

/**
 *
 **/
int DOKAN_CALLBACK geodbfs_setFileTime(LPCWSTR path, CONST FILETIME* creationTime, CONST FILETIME* lastAccessTime, CONST FILETIME* lastWriteTime, PDOKAN_FILE_INFO info) { return 0; }
int DOKAN_CALLBACK geodbfs_setFileAttributes(LPCWSTR path, DWORD FileAttributes, PDOKAN_FILE_INFO dokanFileInfo) { return 0; }
int DOKAN_CALLBACK geodbfs_mkdir(LPCWSTR path, PDOKAN_FILE_INFO dokanFileInfo) { return 0; }
int DOKAN_CALLBACK geodbfs_unlink(LPCWSTR path, PDOKAN_FILE_INFO dokanFileInfo) { return 0; }
int DOKAN_CALLBACK geodbfs_unmount(PDOKAN_FILE_INFO	dokanFileInfo) { return 0; }
int DOKAN_CALLBACK geodbfs_setAllocationSize(LPCWSTR path, LONGLONG AllocSize, PDOKAN_FILE_INFO dokanFileInfo) { return 0; }

/**
 *
 **/
int geodbfs_init(int argc, char* argv[], PFilesystem filesystem){
    pFilesystem = filesystem;

	/* Set filetime */
	SYSTEMTIME st;
    GetSystemTime(&st);              // Gets the current system time
    SystemTimeToFileTime(&st, &filetime);  // Converts the current system time to file time format

	PDOKAN_OPERATIONS dokanOperations = (PDOKAN_OPERATIONS)malloc(sizeof(DOKAN_OPERATIONS));
	PDOKAN_OPTIONS dokanOptions = (PDOKAN_OPTIONS)malloc(sizeof(DOKAN_OPTIONS));

    ZeroMemory(dokanOptions, sizeof(DOKAN_OPTIONS));
    ZeroMemory(dokanOperations, sizeof(DOKAN_OPERATIONS));

    //dokanOptions->Options |= DOKAN_OPTION_KEEP_ALIVE;
    //dokanOptions->Options |= DOKAN_OPTION_REMOVABLE;

	dokanOptions->Version = DOKAN_VERSION;
    dokanOptions->ThreadCount = 0; // default

	WCHAR wideMountPoint[MAX_PATH];

	String mountPoint = argv[argc-1];

	wcscpy_s(wideMountPoint, str2wstr(mountPoint).c_str());
    dokanOptions->MountPoint = wideMountPoint; // set mount point


	dokanOperations->OpenDirectory   = geodbfs_opendir;
	dokanOperations->CreateDirectory = geodbfs_mkdir;
    dokanOperations->FindFiles       = geodbfs_readdir;
	dokanOperations->FindFilesWithPattern = NULL;

    dokanOperations->CreateFile = geodbfs_open;
	dokanOperations->ReadFile   = geodbfs_read;
	dokanOperations->WriteFile  = geodbfs_write;
	dokanOperations->CloseFile  = geodbfs_close;
	dokanOperations->DeleteFile = geodbfs_unlink;
    dokanOperations->Cleanup    = geodbfs_release;
    dokanOperations->SetEndOfFile = geodbfs_setAllocationSize;

	dokanOperations->SetFileTime = geodbfs_setFileTime;
	dokanOperations->SetFileAttributes = geodbfs_setFileAttributes;
	dokanOperations->SetAllocationSize = geodbfs_setAllocationSize;

	dokanOperations->GetFileInformation = geodbfs_getattr;
	dokanOperations->GetVolumeInformation = geodbfs_getVolumeInformation;

	dokanOperations->Unmount = geodbfs_unmount;

    int status = DokanMain(dokanOptions, dokanOperations);
	switch(status)
	{
		case DOKAN_DRIVE_LETTER_ERROR:
			cout << "[dokan] Bad Drive letter"<< endl;
			cout << "[dokan] mount point: " << argv[argc-1] << endl;
			status = -1;
			break;
		case DOKAN_START_ERROR:
			cout << "[dokan] Driver Error something is wrong" << endl;
			status = -1;
			break;
		case DOKAN_MOUNT_ERROR:
			cout << "[dokan] mount point or drive letter is wrong (already used?)" << endl;
			cout << "[dokan] mount point: " << argv[argc-1] << endl;
			status = -1;
			break;
		case DOKAN_MOUNT_POINT_ERROR:
			cout << "[dokan] mount point is invalid" << endl;
			cout << "[dokan] mount point: " << argv[argc-1] << endl;
			status = -1;
			break;
		case DOKAN_ERROR:
			cout << "[dokan] Unknow error!" << endl;
	}
	free(dokanOptions);
	free(dokanOperations);
	return status;
}
#endif
